﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Transactions;
using Microsoft.SqlServer.Server;
using Player.Base;

namespace Player.StoreProcedure
{
    public static class SqlTrace
    {
        /// <summary>
        /// Name of SQL application lock for trace status.
        /// </summary>
        private static readonly string TraceStatusLockName = typeof(SqlTrace).FullName + ".StatusLock";

        /// <summary>
        /// Resultset definition for TraceGetFiles, not real table. 
        /// </summary>
        [Table("TraceGetFilesResultset", true)]
        public enum TraceGetFilesResultset
        {
            /// <summary>
            /// Trace file.
            /// </summary>
            [Column(SqlDbType.NVarChar, 245)]
            TraceFile
        }

        /// <summary>
        /// Gets the running status of SQL trace for application debug.
        /// </summary>
        /// <param name="traceId">
        /// The ID of SQL trace for application debug. Output 0 if the trace does not exists.
        /// </param>
        /// <param name="enabled">if set to <c>true</c> if the trace exists and is enabled.</param>
        public static void GetStatus(out int traceId, out bool enabled)
        {
            StringBuilder sqlBuilder = new StringBuilder();
            sqlBuilder.AppendLine(@"SELECT TOP 1 @traceId = id, @enabled = status");
            sqlBuilder.AppendLine(@"FROM sys.traces");
            sqlBuilder.AppendLine(@"WHERE is_default = 0 AND path LIKE " + TraceFile.PathSqlPatternText);
            sqlBuilder.AppendLine(@"SET @traceId = ISNULL(@traceId, 0)");
            sqlBuilder.AppendLine(@"SET @enabled = ISNULL(@enabled, 0)");
            string sql = sqlBuilder.ToString();
            using (SqlCommand command = SqlHelper.CreateCommand(sql))
            {
                command.Parameters.Add(new SqlParameter("@traceId", SqlDbType.Int) { Direction = ParameterDirection.Output });
                command.Parameters.Add(new SqlParameter("@enabled", SqlDbType.Bit) { Direction = ParameterDirection.Output });
                SqlContext.Pipe.ExecuteAndSend(command);
                traceId = (int)command.Parameters["@traceId"].Value;
                enabled = (bool)command.Parameters["@enabled"].Value;
            }
        }

        /// <summary>
        /// Enables the SQL trace for application debug.
        /// </summary>
        public static void Enable()
        {
            using (TransactionScope transactionScope = new TransactionScope(TransactionScopeOption.Required))
            {
                // Synchronize trace status change with SQL application lock
                LockTraceStatus();

                // Check whether trace already exists and enabled
                int traceId = 0;
                bool traceEnabled = false;
                GetStatus(out traceId, out traceEnabled);

                if (traceId == 0)
                {
                    // If trace does not exists, create it...
                    StringBuilder sqlBuilder = new StringBuilder();
                    sqlBuilder.AppendLine(@"DECLARE @TRACE_FILE_ROLLOVER int");
                    sqlBuilder.AppendLine(@"DECLARE @on bit");
                    sqlBuilder.AppendLine(@"DECLARE @maxFileSize bigint");
                    sqlBuilder.AppendLine(@"DECLARE @fileCount int");
                    sqlBuilder.AppendLine(@"DECLARE @databaseId int");
                    sqlBuilder.AppendLine(@"DECLARE @traceId int");
                    sqlBuilder.AppendLine(@"DECLARE @databaseFilePath nvarchar(260)");
                    sqlBuilder.AppendLine(@"DECLARE @traceFile nvarchar(245)");

                    sqlBuilder.AppendLine(@"SET @TRACE_FILE_ROLLOVER = 2");
                    sqlBuilder.AppendLine(@"SET @on = 1");
                    sqlBuilder.AppendLine(@"SET @maxFileSize = " + (TraceFile.TotalMaxSize / TraceFile.Count).ToString());
                    sqlBuilder.AppendLine(@"SET @fileCount = " + TraceFile.Count.ToString());
                    sqlBuilder.AppendLine(@"SET @databaseId = db_id()");

                    // Get parent folder of current database data file
                    sqlBuilder.AppendLine(@"SELECT TOP 1 @databaseFilePath = SUBSTRING(physical_name, 0, LEN(physical_name) - CHARINDEX(N'\', REVERSE(physical_name)) + 2) " +
                                          @"from sys.database_files where type = 0");

                    // Specify trace file to locate in the same folder as data file
                    sqlBuilder.AppendLine(string.Format(@"SET @traceFile = @databaseFilePath + {0}", TraceFile.NewNameSqlText));

                    // TODO: DB Tracing - Define constants for hardcode names of SQL Trace stored procedure and constants

                    // Create the trace
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_create @traceId OUTPUT, " +
                                          @"@options = @TRACE_FILE_ROLLOVER, @tracefile = @traceFile, " +
                                          @"@maxfilesize = @maxFileSize, @stoptime = NULL, @filecount = @fileCount");

                    // Set events to be capture
                    // SP:Starting
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 42, 14, @on");   // StartTime
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 42, 64, @on");   // SessionLoginName
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 42, 8, @on");    // HostName
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 42, 9, @on");    // ClientProcessID
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 42, 12, @on");   // SPID
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 42, 11, @on");   // LoginName
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 42, 1, @on");    // TextData
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 42, 34, @on");   // ObjectName
                    // SQL:StmtStarting
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 44, 14, @on");   // StartTime
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 44, 64, @on");   // SessionLoginName
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 44, 8, @on");    // HostName
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 44, 9, @on");    // ClientProcessID
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 44, 12, @on");   // SPID
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 44, 11, @on");   // LoginName
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 44, 1, @on");    // TextData
                    // User Error Message
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 162, 14, @on");   // StartTime
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 162, 64, @on");   // SessionLoginName
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 162, 8, @on");    // HostName
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 162, 9, @on");    // ClientProcessID
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 162, 12, @on");   // SPID
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 162, 11, @on");   // LoginName
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 162, 1, @on");    // TextData
                    // Configurable:0
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 82, 14, @on");   // StartTime
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 82, 8, @on");    // HostName
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 82, 64, @on");   // SessionLoginName
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 82, 9, @on");    // ClientProcessID
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 82, 12, @on");   // SPID
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 82, 11, @on");   // LoginName
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setevent @traceId, 82, 1, @on");    // TextData

                    // Set event filters
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setfilter @traceId, 3, 0, 0, @databaseId");   // DatabaseID
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setfilter @traceId, 10, 0, 0, N'.Net SqlClient Data Provider'");   // ApplicationName
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setfilter @traceId, 11, 0, 1, N'sa'");    // LoginName
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setfilter @traceId, 34, 0, 7, N'sp_%'");  // ObjectName

                    // Start the trace...
                    sqlBuilder.AppendLine(@"EXECUTE sp_trace_setstatus @traceId, 1");

                    string sql = sqlBuilder.ToString();
                    using (SqlCommand command = SqlHelper.CreateCommand(sql))
                    {
                        SqlContext.Pipe.ExecuteAndSend(command);
                    }
                }
                else
                {
                    // If trace already exists...
                    if (!traceEnabled)
                    {
                        // If trace is not enabled, start the trace...
                        string sql = @"EXECUTE sp_trace_setstatus @traceId, 1";
                        using (SqlCommand command = SqlHelper.CreateCommand(sql))
                        {
                            command.Parameters.Add(new SqlParameter("@traceId", traceId));
                            SqlContext.Pipe.ExecuteAndSend(command);
                        }
                    }
                }

                transactionScope.Complete();
            }
        }

        /// <summary>
        /// Disables the SQL trace for application debug.
        /// </summary>
        public static void Disable()
        {
            using (TransactionScope transactionScope = new TransactionScope(TransactionScopeOption.Required))
            {
                // Synchronize trace status change with SQL application lock
                LockTraceStatus();

                // Check trace status
                int traceId = 0;
                bool traceEnabled = false;
                GetStatus(out traceId, out traceEnabled);

                if (traceId > 0 && traceEnabled)
                {
                    // If trace exists and is enabled, stop the trace...
                    string sql = @"EXECUTE sp_trace_setstatus @traceId, 0";
                    using (SqlCommand command = SqlHelper.CreateCommand(sql))
                    {
                        command.Parameters.Add(new SqlParameter("@traceId", traceId));
                        SqlContext.Pipe.ExecuteAndSend(command);
                    }
                }

                transactionScope.Complete();
            }
        }

        /// <summary>
        /// Gets the trace files of SQL trace for application debug.
        /// </summary>
        public static void GetTraceFiles()
        {
            // Get export folder path and database name
            string databasefilePath;
            string databaseName;
            StringBuilder sqlFileInfoBuilder = new StringBuilder();
            sqlFileInfoBuilder.AppendLine(@"SELECT TOP 1 @databaseFilePath = SUBSTRING(physical_name, 0, LEN(physical_name) - CHARINDEX(N'\', REVERSE(physical_name)) + 2) " +
                                          @"from sys.database_files where type = 0");
            sqlFileInfoBuilder.AppendLine(@"SET @databaseName = DB_NAME()");
            using (SqlCommand command = SqlHelper.CreateCommand(sqlFileInfoBuilder.ToString()))
            {
                command.Parameters.Add(new SqlParameter("@databaseFilePath", SqlDbType.NVarChar, 260) { Direction = ParameterDirection.Output });
                command.Parameters.Add(new SqlParameter("@databaseName", SqlDbType.NVarChar, 255) { Direction = ParameterDirection.Output });
                SqlContext.Pipe.ExecuteAndSend(command);
                databasefilePath = (string)command.Parameters["@databaseFilePath"].Value;
                databaseName = (string)command.Parameters["@databaseName"].Value;
            }

            // Get and sort list of trace file names
            // Including any trace files of previous SQL Traces, which may happen after SQL Server restarts
            string[] traceFiles = Directory.GetFiles(databasefilePath, TraceFile.GetNamePattern(databaseName));
            Array.Sort<string>(traceFiles, CompareTraceFileName);

            // Return SQL records
            SqlDataRecord record = new SqlDataRecord(new SqlMetaData("TraceFile", SqlDbType.NVarChar, 245));
            SqlContext.Pipe.SendResultsStart(record);
            foreach (string traceFile in traceFiles)
            {
                record.SetString(0, traceFile);
                SqlContext.Pipe.SendResultsRow(record);
            }

            SqlContext.Pipe.SendResultsEnd();
        }

        /// <summary>
        /// Exports trace data from the specified trace file within the specified criteria.
        /// </summary>
        /// <param name="traceFile">The trace file to be export.</param>
        /// <param name="fromDateTime">Date time from which trace data is exported.</param>
        /// <param name="toDateTime">Date time to which trace data is exported..</param>
        /// <param name="sessionLoginNames">The session login names.</param>
        /// <param name="hostNames">The host names.</param>
        public static void Export(string traceFile, DateTime fromDateTime, DateTime toDateTime, string sessionLoginNames, string hostNames)
        {
            IList<string> sessionLoginNameList = SqlHelper.MulitpleValueString.ConvertFromInput(sessionLoginNames);
            IList<string> hostNameList = SqlHelper.MulitpleValueString.ConvertFromInput(hostNames);

            StringBuilder traceSelectSqlBuilder = new StringBuilder();
            traceSelectSqlBuilder.AppendLine(@"SELECT StartTime, SessionLoginName, HostName, ClientProcessID, SPID, LoginName, TextData, ObjectName");
            traceSelectSqlBuilder.AppendLine(string.Format(@"FROM fn_trace_gettable('{0}', 1)", traceFile));
            traceSelectSqlBuilder.AppendLine(@"WHERE StartTime BETWEEN @fromDateTime AND @toDateTime");
            traceSelectSqlBuilder.AppendLine(SqlHelper.MulitpleValueString.BuildWhereInClause("AND {0}", "SessionLoginName", sessionLoginNameList));
            traceSelectSqlBuilder.AppendLine(SqlHelper.MulitpleValueString.BuildWhereInClause("AND {0}", "HostName", hostNameList));

            string sql = traceSelectSqlBuilder.ToString();
            using (SqlCommand command = SqlHelper.CreateCommand(sql))
            {
                command.Parameters.Add(new SqlParameter("@fromDateTime", fromDateTime) { Direction = ParameterDirection.Input });
                command.Parameters.Add(new SqlParameter("@toDateTime", toDateTime) { Direction = ParameterDirection.Input });
                SqlHelper.MulitpleValueString.AddWhereInClauseParameters(command, "SessionLoginName", sessionLoginNameList);
                SqlHelper.MulitpleValueString.AddWhereInClauseParameters(command, "HostName", hostNameList);
                SqlContext.Pipe.ExecuteAndSend(command);
            }
        }

        /// <summary>
        /// Extension method to write trace for a context connection is already present.
        /// If the given connection is not a context connection, it will create one for writing trace.
        /// </summary>
        /// <param name="connection">The connection, supposed to be context connection.</param>
        /// <param name="format">The format of message.</param>
        /// <param name="args">The arguments of message.</param>
        /// <example>
        /// using (SqlConnection connection = new SqlConnection("context connection=true"))
        /// {
        ///     connection.Open();
        ///     connection.Trace("Message here");
        /// }
        /// </example>
        /// <remarks>
        /// 1.
        /// Nested context connection will result in exception.
        /// If there is already an opened context connection, you must pass it as connection argument.
        /// Otherwise, the trace cannot be written successfully.
        /// 2.
        /// If there is still an opened SqlDataReader using the given connection, the trace will also fail.
        /// Try to call this method after closing the SqlDataReader.
        /// </remarks>
        public static void Trace(this SqlConnection connection, string format, params object[] args)
        {
            string message = string.Format(format, args);
            if (SqlContext.IsAvailable)
            {
                // If running within SQL Server... 
                if (IsContextConnection(connection))
                {
                    // If context connection, use it to write trace...
                    WriteWithContextConnection(connection, message);
                }
                else
                {
                    // Otherwise, create new context connection and write trace...
                    WriteWithNewContextConnection(message);
                }
            }
            else
            {
                // Else not running in SQL Server...

                if (SqlHelper.Settings.TraceHandler != null)
                {
                    SqlHelper.Settings.TraceHandler(message);
                }
                else
                {
                    Debug.WriteLine(message);
                }
            }
        }

        /// <summary>
        /// Determines whether the specified connection is context connection.
        /// </summary>
        /// <param name="connection">The connection.</param>
        /// <returns>
        /// <c>true</c> if the specified connection is context connection; otherwise, <c>false</c>.
        /// </returns>
        private static bool IsContextConnection(SqlConnection connection)
        {
            return connection != null
                && string.Compare(connection.ConnectionString, SqlHelper.CONTEXT_CONNECTION, true) == 0;
        }

        /// <summary>
        /// Writes database trace message with creation of new context connection.
        /// </summary>
        /// <param name="message">The message.</param>
        /// <remarks>
        /// Trace write may fail without any exception thrown from this method.
        /// </remarks>
        private static void WriteWithNewContextConnection(string message)
        {
            try
            {
                using (SqlConnection contextConnection = new SqlConnection(SqlHelper.CONTEXT_CONNECTION))
                {
                    contextConnection.Open();
                    WriteWithContextConnection(contextConnection, message);
                }
            }
            catch (Exception ex)
            {
                // Ignore exception
                HandleWriteException(ex);
            }
        }

        /// <summary>
        /// Writes database trace message using the given context connection.
        /// </summary>
        /// <param name="contextConnection">The context connection.</param>
        /// <param name="message">The message.</param>
        /// <remarks>
        /// Trace write may fail without any exception thrown from this method.
        /// </remarks>
        private static void WriteWithContextConnection(SqlConnection contextConnection, string message)
        {
            try
            {
                // TODO: DB Tracing - Define constants parameter length of SQL Trace stored procedure
                const int ChunkSize = 128;
                int messageLength = message.Length;
                if (messageLength <= ChunkSize)
                {
                    // This condition is handled as it is expected most custom messages are not too long...
                    GenerateTraceEvent(contextConnection, message);
                }
                else
                {
                    // In case message is longer than parameter length of SQL Trace stored procedure, 
                    // split and write chunks of trace messages...
                    int startIndex = 0;
                    int actualLength = 0;
                    string messageChunk = null;
                    while (startIndex < messageLength)
                    {
                        actualLength = (startIndex + ChunkSize > messageLength) ? (messageLength - startIndex) : ChunkSize;
                        messageChunk = message.Substring(startIndex, actualLength);
                        GenerateTraceEvent(contextConnection, messageChunk);
                        startIndex += ChunkSize;
                    }
                }
            }
            catch (Exception ex)
            {
                // Ignore exception
                HandleWriteException(ex);
            }
        }

        /// <summary>
        /// Generates the configurable SQL Trace event.
        /// </summary>
        /// <param name="contextConnection">The context connection.</param>
        /// <param name="message">The message.</param>
        private static void GenerateTraceEvent(SqlConnection contextConnection, string message)
        {
            using (SqlCommand command = SqlHelper.CreateCommand(contextConnection, "sp_trace_generateevent"))
            {
                // TODO: DB Tracing - Define constants for hardcode names of SQL Trace stored procedure
                command.CommandType = CommandType.StoredProcedure;
                command.Parameters.Add(new SqlParameter("@event_class", 82));
                command.Parameters.Add(new SqlParameter("@userinfo", message));
                command.ExecuteNonQuery();
            }
        }

        /// <summary>
        /// Locks the trace status with SQL application lock.
        /// </summary>
        /// <remarks>
        /// Use this method within a transaction. The lock will be released after transaction commit or rollback.
        /// </remarks>
        private static void LockTraceStatus()
        {
            string sqlLock = @"EXECUTE sp_getapplock @Resource = @traceStatusLockName, @LockMode = 'Exclusive';";
            using (SqlCommand command = SqlHelper.CreateCommand(sqlLock))
            {
                command.Parameters.Add(new SqlParameter("@traceStatusLockName", TraceStatusLockName));
                SqlContext.Pipe.ExecuteAndSend(command);
            }
        }

        /// <summary>
        /// Compares the name of the SQL Trace file.
        /// </summary>
        /// <param name="path1">The first path to compare.</param>
        /// <param name="path2">The second path to compare.</param>
        /// <returns>
        /// Value Condition Less than 0, if path1 is less than path2.  
        /// 0, if path1 equals path2.  
        /// Greater than 0, if path1 is greater than path2.
        /// </returns>
        /// <remarks>
        /// Get and sort list of trace file names, such that in ascending order:
        ///     Database20090101001122333.trc, Database20090101001122333_1.trc, 
        ///     Database20101006013045123.trc, Database20101006013045123_1.trc, 
        ///     Database20101006013045123_9.trc, Database20101006013045123_10.trc
        /// </remarks>
        private static int CompareTraceFileName(string path1, string path2)
        {
            string fileName1 = Path.GetFileNameWithoutExtension(path1);
            string fileName2 = Path.GetFileNameWithoutExtension(path2);

            int result = string.Compare(GetTraceGroupName(fileName1), GetTraceGroupName(fileName2), true);
            if (result == 0)
            {
                // If prefix are the same, compare suffix...
                // Compare length such that "..._9.trc" < "..._10.trc"
                int length = fileName1.Length - fileName2.Length;
                result = (length == 0) ? string.Compare(fileName1, fileName2, true) : length;
            }

            return result;
        }

        /// <summary>
        /// Gets the name of the trace group.
        /// </summary>
        /// <param name="fileName">Name of the file.</param>
        /// <returns>The name of the trace group.</returns>
        private static string GetTraceGroupName(string fileName)
        {
            // Remove the suffix after file name, e.g. Database20090101001122333_1 => Database20090101001122333
            return Regex.Replace(fileName, "_[0-9]*$", string.Empty);
        }

        /// <summary>
        /// Handles the write exception.
        /// </summary>
        /// <param name="exception">The exception.</param>
        private static void HandleWriteException(Exception exception)
        {
            // TODO: DB Tracing - should write to file or event log?
        }

        /// <summary>
        /// Defines trace file properties.
        /// </summary>
        private static class TraceFile
        {
            /// <summary>
            /// Total maximum size in MB of trace files.
            /// </summary>
            public static readonly int TotalMaxSize = 800;

            /// <summary>
            /// Number of split trace files.
            /// </summary>
            public static readonly int Count = 10;

            /// <summary>
            /// Prefix of file name of SQL trace for application debug.
            /// </summary>
            public static readonly string TraceFilePrefix = "Centrify.DirectAudit.";

            /// <summary>
            /// Gets the SQL text of new trace file.
            /// </summary>
            /// <value>The SQL text of new trace file.</value>
            /// <remarks>
            /// New trace file name format: [Prefix][DatabaseName][yyyyMMddHHmmssfff]
            /// E.g. "N'Centrify.DirectAudit.Database20000101112233444'"
            /// </remarks>
            public static string NewNameSqlText
            {
                get
                {
                    return string.Format(
                        @"N'{0}' + DB_NAME() + CONVERT(VARCHAR, GETDATE(),112) + REPLACE(CONVERT(VARCHAR, GETDATE(),114),':','')",
                        TraceFilePrefix);
                }
            }

            /// <summary>
            /// Gets the SQL pattern text for search any trace files.
            /// </summary>
            /// <value>The path SQL pattern text for search any trace files.</value>
            /// <remarks>Example: "N'%Centrify.DirectAudit.Database%'"</remarks>
            public static string PathSqlPatternText
            {
                get
                {
                    return string.Format(@"N'%{0}' + DB_NAME() + '%'", TraceFilePrefix);
                }
            }

            /// <summary>
            /// Gets the name pattern for search in file system.
            /// </summary>
            /// <param name="databaseName">Name of the database.</param>
            /// <returns>The name pattern for search in file system.</returns>
            /// <remarks>Example: "Centrify.DirectAudit.Database*.trc"</remarks>
            public static string GetNamePattern(string databaseName)
            {
                return string.Format("{0}{1}*.trc", TraceFilePrefix, databaseName);
            }
        }
    }
}
